import threading
from abc import abstractmethod
from copy import copy

from .base import Detector
from ..base import Property
from ..buffered_generator import BufferedGenerator


class _VideoAsyncBoxDetector(Detector):
    """Video Async Detector Abstract Class

    Abstract class for use with Video based detectors, which includes an async
    option where video frames will be dropped in order to process the latest
    frame (applicable when processing live video).
    """

    run_async: bool = Property(
        doc="If set to ``True``, the detector will digest frames from the reader asynchronously "
            "and only perform detection on the last frame digested. This is suitable when the "
            "detector is applied to readers generating a live feed (e.g. "
            ":class:`~.FFmpegVideoStreamReader`), where real-time processing is paramount. "
            "Defaults to ``False``",
        default=False)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # Variables used in async mode
        if self.run_async:
            self._buffer = None
            # Initialise frame capture thread
            self._capture_thread = threading.Thread(target=self._capture)
            self._capture_thread.daemon = True
            self._thread_lock = threading.Lock()
            self._capture_thread.start()

    @BufferedGenerator.generator_method
    def detections_gen(self):
        """Returns a generator of detections for each frame.

        Yields
        ------
        : :class:`datetime.datetime`
            Datetime of current time step
        : set of :class:`~.Detection`
            Detections generated in the time step. The detection state vector is of the form
            ``(x, y, w, h)``, where ``x, y`` denote the relative coordinates of the top-left
            corner of the bounding box containing the object, while ``w, h`` denote the relative
            width and height of the bounding box. Additionally, each detection carries the
            following meta-data fields:

            - ``raw_box``: The raw bounding box, as generated by TensorFlow.
            - ``class``: A dict with keys ``id`` and ``name`` relating to the \
              id and name of the detection class, as specified by the label map.
            - ``score``: A float in the range ``(0, 1]`` indicating the detector's confidence

        """
        if self.run_async:
            yield from self._detections_gen_async()
        else:
            yield from self._detections_gen()

    def _capture(self):
        for timestamp, frame in self.sensor:
            self._thread_lock.acquire()
            self._buffer = frame
            self._thread_lock.release()

    def _detections_gen(self):
        for timestamp, frame in self.sensor:
            detections = self._get_detections_from_frame(frame)
            yield timestamp, detections

    def _detections_gen_async(self):
        while self._capture_thread.is_alive():
            if self._buffer is not None:
                self._thread_lock.acquire()
                frame = copy(self._buffer)
                self._buffer = None
                self._thread_lock.release()

                detections = self._get_detections_from_frame(frame)

                yield frame.timestamp, detections

    @abstractmethod
    def _get_detections_from_frame(self, frame):
        raise NotImplementedError()
